{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Delegating to Parent"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You'll most likely encounter `super()` in the `__init__` method of custom classes, but delegation is not restricted to `__init__`. You can use `super()` anywhere you need to explicitly instruct Python to use a callable definition that is higher up in the inheritance chain. In these cases you only need to use `super()` if there is some ambiguity - i.e. your current class overrides an ancestor's callable and you need to specifically tell Python to use the callable in the ancestry chain."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Person:\n",
    "    def work(self):\n",
    "        return 'Person works...'\n",
    "    \n",
    "class Student(Person):\n",
    "    def work(self):\n",
    "        result = super().work()\n",
    "        return f'Student works... and {result}'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "s = Student()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'Student works... and Person works...'"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "s.work()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now the `super().work()` call in the `Student` class looks up the hierarchy chain until it finds the first definition for that callable.\n",
    "\n",
    "We can easily see this:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Person:\n",
    "    def work(self):\n",
    "        return 'Person works...'\n",
    "    \n",
    "class Student(Person):\n",
    "    pass\n",
    "\n",
    "class PythonStudent(Student):\n",
    "    def work(self):\n",
    "        result = super().work()\n",
    "        return f'PythonStudent codes... and {result}'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "ps = PythonStudent()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'PythonStudent codes... and Person works...'"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "ps.work()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Of course every class can delegate up the chain in turn:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Person:\n",
    "    def work(self):\n",
    "        return 'Person works...'\n",
    "    \n",
    "class Student(Person):\n",
    "    def work(self):\n",
    "        result = super().work()\n",
    "        return f'Student studies... and {result}'\n",
    "    \n",
    "class PythonStudent(Student):\n",
    "    def work(self):\n",
    "        result = super().work()\n",
    "        return f'PythonStudent codes... and {result}'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'PythonStudent codes... and Student studies... and Person works...'"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "ps = PythonStudent()\n",
    "ps.work()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Do note that when there is **no ambiguity** there is no need to use `super()`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Person:\n",
    "    def work(self):\n",
    "        return 'Person works...'\n",
    "    \n",
    "class Student(Person):\n",
    "    def study(self):\n",
    "        return 'Student studies...'\n",
    "    \n",
    "class PythonStudent(Student):\n",
    "    def code(self):\n",
    "        result_1 = self.work()\n",
    "        result_2 = self.study()\n",
    "        return f'{result_1} and {result_2} and PythonStudent codes...'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "ps = PythonStudent()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'Person works... and Student studies... and PythonStudent codes...'"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "ps.code()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The really important thing to understand is which object (instance) is bound when a delegated method is called. It is **always** the calling object:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Person:\n",
    "    def work(self):\n",
    "        return f'{self} works...'\n",
    "    \n",
    "class Student(Person):\n",
    "    def work(self):\n",
    "        result = super().work()\n",
    "        return f'{self} studies... and {result}'\n",
    "\n",
    "class PythonStudent(Student):\n",
    "    def work(self):\n",
    "        result = super().work()\n",
    "        return f'{self} codes... and {result}'\n",
    "    "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "ps = PythonStudent()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'0x7fd388308f98'"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "hex(id(ps))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'<__main__.PythonStudent object at 0x7fd388308f98> codes... and <__main__.PythonStudent object at 0x7fd388308f98> studies... and <__main__.PythonStudent object at 0x7fd388308f98> works...'"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "ps.work()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As you can see each of the methods in the parent classes were called bound to the original `PythonStudent` instance `ps`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "What this means is that when a class sets an instance attribute, it will be set in the namespace of the original object. Here's a simple example that illustrates this:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Person:\n",
    "    def set_name(self, value):\n",
    "        print('Setting name using Person set_name method...')\n",
    "        self.name = value\n",
    "        \n",
    "class Student(Person):\n",
    "    def set_name(self, value):\n",
    "        print('Student class delegating back to parent...')\n",
    "        super().set_name(value)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [],
   "source": [
    "s = Student()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As you can see, the dictionary for `s` is currently empty:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{}"
      ]
     },
     "execution_count": 18,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "s.__dict__"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "But if we call set_name:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Student class delegating back to parent...\n",
      "Setting name using Person set_name method...\n"
     ]
    }
   ],
   "source": [
    "s.set_name('Eric')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As you can see the `Person` class `set_name` method did the actual work, but the `name` attribute is created in the `Student` instance `s`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'name': 'Eric'}"
      ]
     },
     "execution_count": 20,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "s.__dict__"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So just to re-emphasize, whenever you use `super()`, any `self` in the called methods actually refers to the object used to make the initial call."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "One place where this is really handy is in class initialization - we use it to leverage the parent class initializer so we don't have to re-write a lot of initialization code in our child class."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's use a simple example first:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Person:\n",
    "    def __init__(self, name):\n",
    "        self.name = name\n",
    "        \n",
    "class Student(Person):\n",
    "    def __init__(self, name, student_number):\n",
    "        super().__init__(name)\n",
    "        self.student_number = student_number"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [],
   "source": [
    "s = Student('Python', 30)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'name': 'Python', 'student_number': 30}"
      ]
     },
     "execution_count": 23,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "s.__dict__"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "I do want to point out that if your parent class has initializer and your child class does not, then Python will attempt to call the parent `__init__` automatically - because the `__init__` is **inherited** from the parent class!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Person:\n",
    "    def __init__(self):\n",
    "        print('Person __init__')\n",
    "        \n",
    "class Student(Person):\n",
    "    pass"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Person __init__\n"
     ]
    }
   ],
   "source": [
    "s = Student()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "But watch what happens if the parent class requires an argument:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Person:\n",
    "    def __init__(self, name):\n",
    "        print('Person __init__ called...')\n",
    "        self.name = name\n",
    "        \n",
    "class Student(Person):\n",
    "    pass"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "__init__() missing 1 required positional argument: 'name'\n"
     ]
    }
   ],
   "source": [
    "try:\n",
    "    s = Student()\n",
    "except TypeError as ex:\n",
    "    print(ex)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In fact, we can pass this argument to the `Student` class and Python will automatically pass it along to the (inherited) `Person` class `__init__`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Person __init__ called...\n"
     ]
    }
   ],
   "source": [
    "s = Student('Alex')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'name': 'Alex'}"
      ]
     },
     "execution_count": 29,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "s.__dict__"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "However, if we provide a custom `__init__` in our child class, then Python will not automatically call the parent init:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Person:\n",
    "    def __init__(self):\n",
    "        print('Person __init__ called...')\n",
    "        \n",
    "class Student(Person):\n",
    "    def __init__(self):\n",
    "        print('Student __init__ called...')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Student __init__ called...\n"
     ]
    }
   ],
   "source": [
    "s = Student()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "To do so, we need to call `super().__init__`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Person:\n",
    "    def __init__(self):\n",
    "        print('Person __init__ called...')\n",
    "\n",
    "class Student(Person):\n",
    "    def __init__(self):\n",
    "        super().__init__()\n",
    "        print('Student __init__ called...')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Person __init__ called...\n",
      "Student __init__ called...\n"
     ]
    }
   ],
   "source": [
    "s = Student()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's take a look at a more practical example:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's first create a `Circle` class:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "metadata": {},
   "outputs": [],
   "source": [
    "from math import pi\n",
    "from numbers import Real\n",
    "\n",
    "class Circle:\n",
    "    def __init__(self, r):\n",
    "        self._r = r\n",
    "        self._area = None\n",
    "        self._perimeter = None\n",
    "        \n",
    "    @property\n",
    "    def radius(self):\n",
    "        return self._r\n",
    "    \n",
    "    @radius.setter\n",
    "    def radius(self, r):\n",
    "        if isinstance(r, Real) and r > 0:\n",
    "            self._r = r\n",
    "            self._area = None\n",
    "            self._perimeter = None\n",
    "        else:\n",
    "            raise ValueError('Radius must a positive real number.')\n",
    "            \n",
    "    @property\n",
    "    def area(self):\n",
    "        if self._area is None:\n",
    "            self._area = pi * self.radius ** 2\n",
    "        return self._area\n",
    "            \n",
    "    @property\n",
    "    def perimeter(self):\n",
    "        if self._perimeter is None:\n",
    "            self._perimeter = 2 * pi * self.radius\n",
    "        return self._perimeter"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now let's make a specialized circle class, a `UnitCircle` which is simply a circle with a radius of `1`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 35,
   "metadata": {},
   "outputs": [],
   "source": [
    "class UnitCircle(Circle):\n",
    "    def __init__(self):\n",
    "        super().__init__(1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And now we can use it this way:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "metadata": {},
   "outputs": [],
   "source": [
    "u = UnitCircle()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 37,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(1, 3.141592653589793, 6.283185307179586)"
      ]
     },
     "execution_count": 37,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "u.radius, u.area, u.perimeter"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now one thing that's off here is that we can actually set the radius on the `UnitCircle` - which we probably don't want to allow.\n",
    "\n",
    "My approach here is to redefine the `radius` property in the unit circle class and disallow setting the radius altogether:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 38,
   "metadata": {},
   "outputs": [],
   "source": [
    "class UnitCircle(Circle):\n",
    "    def __init__(self):\n",
    "        super().__init__(1)\n",
    "        \n",
    "    @property\n",
    "    def radius(self):\n",
    "        return super().radius"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 39,
   "metadata": {},
   "outputs": [],
   "source": [
    "u = UnitCircle()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 40,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "1"
      ]
     },
     "execution_count": 40,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "u.radius"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 41,
   "metadata": {},
   "outputs": [
    {
     "ename": "AttributeError",
     "evalue": "can't set attribute",
     "output_type": "error",
     "traceback": [
      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[0;31mAttributeError\u001b[0m                            Traceback (most recent call last)",
      "\u001b[0;32m<ipython-input-41-24f2873b5e0a>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mu\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mradius\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;36m10\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
      "\u001b[0;31mAttributeError\u001b[0m: can't set attribute"
     ]
    }
   ],
   "source": [
    "u.radius = 10"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Note how my overriding property uses `super().radius` - I cannot use `self.radius` as that would be trying to call the radius getter defined in the `UnitCircle` class (the one I am currently defining) - instead I specifically want to access the property from the parent class."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Finally I want to come back to another example that also helps underscore the fact that methods called via `super()` are still bound to the original (child) object, and hence will use methods defined in the child class if they override any in the parent class - this is a little tricky, but fundamental to understand:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Person:\n",
    "    def method_1(self):\n",
    "        print('Person.method_1')\n",
    "        self.method_2()\n",
    "        \n",
    "    def method_2(self):\n",
    "        print('Person.method_2')\n",
    "        \n",
    "class Student(Person):\n",
    "    def method_1(self):\n",
    "        print('Student.method_1')\n",
    "        super().method_1()\n",
    "        "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s = Student()\n",
    "s.method_1()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So `Student.method_1` called `Person.method_1` via `super`, which in turn called `Person.method_2` - all of these methods were bound to the `Student` instance `s`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now watch what happens when we also override `method_2` in the `Student` class:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Person:\n",
    "    def method_1(self):\n",
    "        print('Person.method_1')\n",
    "        self.method_2()\n",
    "        \n",
    "    def method_2(self):\n",
    "        print('Person.method_2')\n",
    "        \n",
    "class Student(Person):\n",
    "    def method_1(self):\n",
    "        print('Student.method_1')\n",
    "        super().method_1()\n",
    "        \n",
    "    def method_2(self):\n",
    "        print('Student.method_2')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s = Student()\n",
    "s.method_1()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Since `self.method_2()` in the Person class was called from `s`, that `self` is the instance `s`, and hence `method_2` from the `Student` class was called, not the one defined in the `Person` class!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
